package org.runtimedao.dao;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.runtimedao.annotation.DAOEntity;
import org.runtimedao.annotation.Query;
import org.runtimedao.compiler.JavaCodeCompiler;
import org.runtimedao.exception.DAOException;
import org.runtimedao.template.DAOMethod;
import org.runtimedao.template.DAOTemplate;
import org.runtimedao.utils.ConstantsUtils;

public class DAOFactory {

	private static final String DEFAULT_PACKAGE_IS_NOT_SUPPORTED_YET = "Default package is not supported yet";
	private static final String DEFAULT_PERSISTENCE_UNIT_LOADED = "Default persistence unit loaded";
	private static final String FAILED_TO_LOAD_DEFAULT_PERSISTENCE_UNIT_MESSAGE = "Failed to load default persistence unit. You must call DAOHelper.init(persistenceUnit) to load another one and DAOHelper.cleanUp() to close it";
	private static final String TRYING_TO_LOAD_DEFAULT_PERSISTENCE_UNIT = "Trying to load default persistence unit: ";
	private static final String DAO_ENTITY_ANNOTATION = "@DAOEntity";
	private static final String INSTANTIATION_FAILED = "Instantiation failed";
	private static final String TEMPLATE_FILE = "templates/DAOClass.java.template";
	private static final String AT_DAO_ENTITY_NOT_FOUND = DAO_ENTITY_ANNOTATION
			+ " not found";

	private static Map<String, Object> daoObjectCache = new HashMap<String, Object>();

	private static Logger LOG = Logger.getLogger(DAOFactory.class);

	/**
	 * Try to load default persistence unit
	 */
	static {
		try {
			LOG.info(TRYING_TO_LOAD_DEFAULT_PERSISTENCE_UNIT
					+ DAOHelper.DEFAULT_PERSISTENCE_UNIT_NAME);
			DAOHelper
					.loadPersistenceUnit(DAOHelper.DEFAULT_PERSISTENCE_UNIT_NAME);
			LOG.info(DEFAULT_PERSISTENCE_UNIT_LOADED);
		} catch (Exception e) {
			LOG.warn(FAILED_TO_LOAD_DEFAULT_PERSISTENCE_UNIT_MESSAGE);
		}
	}

	/**
	 * Compiles or gets from cache a DAO class
	 * 
	 * @param daoClass
	 * @return
	 */
	public static synchronized <T extends DAO<?>> T getDAO(Class<T> daoClass) {
		T daoObject = getFromCache(daoClass);
		if (daoObject != null) {
			return daoObject;
		}

		Class<?> entityClass = getEntityClass(daoClass);
		List<DAOMethod> daoMethods = createDAOMethods(daoClass);

		String className = daoClass.getSimpleName() + "Impl";

		if (!daoClass.getName().contains(ConstantsUtils.DOT)) {
			LOG.error(DEFAULT_PACKAGE_IS_NOT_SUPPORTED_YET);
			throw new NotImplementedException(
					DEFAULT_PACKAGE_IS_NOT_SUPPORTED_YET);
		}

		String packageName = daoClass.getName().substring(0,
				daoClass.getName().lastIndexOf(ConstantsUtils.DOT));

		DAOTemplate template = new DAOTemplate(TEMPLATE_FILE);
		if (StringUtils.isNotBlank(packageName)) {
			template.setParameter("packageName", packageName);
		}
		template.setParameter("className", className);
		template.setParameter("interfaceName", daoClass.getName());
		template.setParameter("entityClassName", entityClass.getName());
		template.setParameter("methods", daoMethods);

		String source = template.merge();

		JavaCodeCompiler<T> compiler = new JavaCodeCompiler<T>();
		Class<T> compiledClass = compiler.compile(packageName, className,
				source);

		try {
			daoObject = compiledClass.newInstance();
		} catch (Exception e) {
			LOG.error(INSTANTIATION_FAILED, e);
			throw new DAOException(INSTANTIATION_FAILED, e);
		}
		putInCache(daoObject);

		return daoObject;
	}

	@SuppressWarnings("unchecked")
	private static <T> T getFromCache(Class<T> daoClass) {
		return (T) daoObjectCache.get(daoClass.getName());
	}

	private static <T> void putInCache(T daoObject) {
		daoObjectCache.put(daoObject.getClass().getName(), daoObject);
	}

	private static <T> Class<?> getEntityClass(Class<T> daoClass) {
		DAOEntity annotation = daoClass.getAnnotation(DAOEntity.class);
		if (annotation == null) {
			LOG.error(AT_DAO_ENTITY_NOT_FOUND);
			throw new DAOException(AT_DAO_ENTITY_NOT_FOUND);
		}

		Class<?> entityClass = annotation.value();
		return entityClass;
	}

	private static <T> List<DAOMethod> createDAOMethods(Class<T> daoClass) {
		Method[] methods = daoClass.getMethods();
		List<DAOMethod> daoMethods = new ArrayList<DAOMethod>();
		for (Method method : methods) {
			Query query = method.getAnnotation(Query.class);
			if (query != null) {
				daoMethods.add(new DAOMethod(method, query.value()));
			}
		}
		return daoMethods;
	}
}
